<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>
      
    线程编程指导 第三章 - Arisu
    
    </title>
    <link rel="shortcut icon" href="https://ushio.oss-cn-shanghai.aliyuncs.com/Arisu/Arisu_Icon.png" type="image/png" />

    
    
    <link href="atom.xml" rel="alternate" title="Arisu" type="application/atom+xml">
    <link rel="stylesheet" href="asset/css/style.min.css">
    <link rel="stylesheet" href="asset/css/doc.css">
    <script src="asset/app.js"></script>
</head>
  <body>
    <section class="hero">
      <div class="hero-head">
          <nav class="navbar" role="navigation" aria-label="main navigation">
              <div class="container">
              <div class="navbar-brand">
                
                <a target="_self" class="navbar-item " href="index.html">主页</a>
                
                <a target="_self" class="navbar-item " href="Apple.html">Apple</a>
                

                <a role="button" id="navbarSNSRssSwitchBtn" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarSNSRssButtons">
                  <span aria-hidden="true"></span>
                  <span aria-hidden="true"></span>
                  <span aria-hidden="true"></span>
                </a>
              </div>
            
              <div id="navbarSNSRssButtons" class="navbar-menu">
                <div class="navbar-start">
                  
                </div>
            
                <div class="navbar-end">
                  <div class="navbar-item">
                    <!--buttons start-->
                    <div class="buttons">
                      
                        
                        
                        
                        
                      
                      <a href="atom.xml" target="_blank" title="RSS">
                          <span class="icon is-large has-text-black-bis">
                              <svg class="svg-inline--fa fa-rss fa-w-14 fa-lg" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="rss" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" data-fa-i2svg=""><path fill="currentColor" d="M128.081 415.959c0 35.369-28.672 64.041-64.041 64.041S0 451.328 0 415.959s28.672-64.041 64.041-64.041 64.04 28.673 64.04 64.041zm175.66 47.25c-8.354-154.6-132.185-278.587-286.95-286.95C7.656 175.765 0 183.105 0 192.253v48.069c0 8.415 6.49 15.472 14.887 16.018 111.832 7.284 201.473 96.702 208.772 208.772.547 8.397 7.604 14.887 16.018 14.887h48.069c9.149.001 16.489-7.655 15.995-16.79zm144.249.288C439.596 229.677 251.465 40.445 16.503 32.01 7.473 31.686 0 38.981 0 48.016v48.068c0 8.625 6.835 15.645 15.453 15.999 191.179 7.839 344.627 161.316 352.465 352.465.353 8.618 7.373 15.453 15.999 15.453h48.068c9.034-.001 16.329-7.474 16.005-16.504z"></path></svg><!-- <i class="fas fa-rss fa-lg"></i> -->
                          </span>
                      </a>
                    </div>
                    <!--buttons end-->

                  </div>
                </div>
                </div>
              </div>
            </nav>
      </div>

 <div class="hero-body ct-body"></div>
      
    </section>
    <section class="ct-body">
      <div class="container">
          <div class="columns is-variable bd-klmn-columns is-4 is-centered">
              <div class="column is-four-fifths">
                  <div class="post-body single-content">
                    
                    <h1 class="title">
                            线程编程指导 第三章   
                      </h1>
                     
                    
                      <div class="media">
                            
                            <figure class="media-left">
                              <p class="image is-48x48">
                                
                                  <img class="is-rounded" src="https://ushio.oss-cn-shanghai.aliyuncs.com/Arisu/Arisu_Icon.png">
                                
                              </p>
                            </figure>
                            
                            <div class="media-content">
                              <div class="content">
                                <p>
                                 <span class="date">2020/02/25</span>
                                  <span class="tran-posted-in">posted in</span>&nbsp; 
                                  
                                      <span class="posted-in"><a href='Apple%20Developer%20Documentation.html'>Apple Developer Documentation</a></span>
                                  
                                      <span class="posted-in"><a href='%E7%BA%BF%E7%A8%8B%E7%BC%96%E7%A8%8B%E6%8C%87%E5%AF%BC.html'>线程编程指导</a></span>
                                         
                                  

                                   
                                      
                                  <br />
                                  <span class="tran-tags">Tags:</span>&nbsp;
                                  
                                    <a class="tag is-link is-light" href='tag_%E6%8A%80%E6%9C%AF%E6%96%87%E6%A1%A3.html'>#技术文档</a>
                                  
                                    <a class="tag is-link is-light" href='tag_Arisu.html'>#Arisu</a>
                                     

                                </p>
                              </div>
                            </div>
                         
                    </div>
                </div>
                  <article class="markdown-body single-content">
                    <h1><a id="%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF%EF%BC%88run-loops%EF%BC%89" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>运行循环（Run Loops）</h1>
<p>运行循环是与线程相关的基础架构的一部分。运行循环是一个事件处理循环，用于计划工作并协调传入事件的接收。<u>运行循环的目的是在有工作时保持线程忙，并在没有线程时让线程进入休眠状态。</u></p>
<p>运行循环管理不是完全自动的。您仍然必须设计线程的代码以在适当的时间启动运行循环并响应传入的事件。Cocoa 和 Core Foundation 都提供了运行循环对象，以帮助您配置和管理线程的运行循环。您的应用程序不需要显式创建这些对象；每个线程（包括应用程序的主线程）都有一个关联的运行循环对象。但是，只有辅助线程需要显式运行其运行循环。作为应用程序启动过程的一部分，应用程序框架会自动在主线程上设置并运行运行循环。</p>
<p>以下部分提供有关运行循环以及如何为应用程序配置它们的更多信息。有关运行循环对象的其他信息，请参阅 <a href="https://developer.apple.com/documentation/foundation/nsrunloop">NSRunLoop 类参考</a>和 <a href="https://developer.apple.com/documentation/corefoundation/cfrunloop">CFRunLoop 参考</a>。</p>
<h2><a id="%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF%E7%9A%84%E5%89%96%E6%9E%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>运行循环的剖析</h2>
<p>运行循环非常类似于其名称意义。它是您的线程进入并用于运行事件处理程序以响应传入事件的循环。您的代码提供了用于实现运行循环的实际循环部分的控制语句 - 换句话说，您的代码提供了驱动运行循环的 while 或 for 循环。在循环中，使用运行循环对象“run”接收事件的事件处理代码并调用已安装的处理程序。</p>
<p><u>运行循环从两种不同类型的源接收事件。</u>输入源提供异步事件，通常是来自另一个线程或来自不同应用程序的消息。定时器源提供发生在预定时间或重复间隔的同步事件。两种类型的源都使用特定于应用程序的处理程序例程来处理事件。</p>
<p>图 3-1 显示了运行循环和各种源的概念结构。输入源将异步事件传递给相应的处理程序，并导致 <code>runUntilDate:</code> 方法（在线程的关联 <code>NSRunLoop</code> 对象上调用）退出。定时器源将事件传递给其处理程序例程，但不会导致运行循环退出。</p>
<p>图 3-1：运行循环的结构及其来源</p>
<p><img src="https://ushio.oss-cn-shanghai.aliyuncs.com/MWeb/Threading_Programming_Guide/runloop.jpg" alt="runloop" /></p>
<p>除了处理输入源之外，运行循环还会生成有关运行循环行为的通知。已注册的运行循环观察器可以接收这些通知并使用它们在线程上执行其他处理。您可以使用 Core Foundation 在线程上安装运行循环观察器。</p>
<p>以下部分提供有关运行循环的组件及其运行模式的更多信息。它们还描述了在处理事件期间的不同时间生成的通知。</p>
<h3><a id="%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF%E6%A8%A1%E5%BC%8F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>运行循环模式</h3>
<p><u>运行循环模式是要监视的输入源和定时器的集合，以及要通知的运行循环观察器的集合。</u>每次运行运行循环时，都指定（显式或隐式）运行的特定“mode”。在运行循环的传递过程中，仅监视与该模式关联的源并允许其传递其事件。（类似地，只有与该模式相关联的观察者被通知运行循环的进度。）与其他模式相关联的源保持任何新事件，直到后续以适当模式通过循环。</p>
<p>在您的代码中，您可以按名称识别模式。Cocoa 和 Core Foundation 都定义了默认模式和几种常用模式，以及用于在代码中指定这些模式的字符串。您只需为模式名称指定自定义字符串即可定义自定义模式。虽然您为自定义模式指定的名称是任意的，但这些模式的内容不是。您必须确保将一个或多个输入源，定时器或运行循环观察器添加到您为其创建的任何模式中才有用。</p>
<p>您可以使用模式在特定的运行循环期间过滤掉不需要的来源中的事件。大多数情况下，您需要在系统定义的“default”模式下运行运行循环。但是，模态面板可能以“modal（模态）”模式运行。在此模式下，只有与模态面板相关的源才会将事件传递给线程。对于辅助线程，您可以使用自定义模式来防止低优先级源在时间关键操作期间传递事件。</p>
<blockquote>
<p>注意：模式根据事件的来源而不是事件的类型进行区分。例如，您不会使用模式仅匹配鼠标按下事件或仅匹配键盘事件。您可以使用模式来侦听不同的端口集，暂时挂起定时器，或以其他方式更改源并运行当前正在监视的循环观察器。</p>
</blockquote>
<p>表 3-1 列出了 Cocoa 和 Core Foundation 定义的标准模式以及何时使用该模式的说明。名称列列出了用于在代码中指定模式的实际常量。</p>
<p>表 3-1：预定义的运行循环模式</p>
<table>
<thead>
<tr>
<th>模式</th>
<th>名称</th>
<th style="text-align: center">描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>Default</td>
<td><code>NSDefaultRunLoopMode</code> (Cocoa) <code>kCFRunLoopDefaultMode</code> (Core Foundation)</td>
<td style="text-align: center">默认模式是用于大多数操作的模式。大多数情况下，您应该使用此模式启动运行循环并配置输入源。</td>
</tr>
<tr>
<td>连接</td>
<td><code>NSConnectionReplyMode</code> (Cocoa)</td>
<td style="text-align: center">Cocoa 将此模式与 <code>NSConnection</code> 对象结合使用以监视回复。您自己应该很少需要使用此模式。</td>
</tr>
<tr>
<td>Model</td>
<td>NSModalPanelRunLoopMode (Cocoa)</td>
<td style="text-align: center">Cocoa 使用此模式来识别用于模态面板的事件。</td>
</tr>
<tr>
<td>Event tracking</td>
<td><code>NSEventTrackingRunLoopMode</code> (Cocoa)</td>
<td style="text-align: center">Cocoa 使用此模式限制鼠标拖动循环和其他类型的用户界面跟踪循环期间的传入事件。</td>
</tr>
<tr>
<td>Common modes</td>
<td><code>NSRunLoopCommonModes</code> (Cocoa) <code>kCFRunLoopCommonModes</code> (Core Foundation)</td>
<td style="text-align: center">这是一组可配置的常用模式。将输入源与此模式相关联也会将其与组中的每个模式相关联。对于 Cocoa 应用程序，此集合默认包括默认，模态和事件跟踪模式。Core Foundation 最初只包含默认模式。您可以使用 <code>CFRunLoopAddCommonMode</code> 函数将自定义模式添加到集合中。</td>
</tr>
</tbody>
</table>
<h3><a id="%E8%BE%93%E5%85%A5%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>输入源</h3>
<p><u>输入源以异步方式向线程传递事件。事件的来源取决于输入源的类型，通常是两个类别之一。基于端口的输入源监视应用程序的 Mach 端口。自定义输入源监视自定义事件源。</u>就运行循环而言，输入源是基于端口还是自定义无关紧要。系统通常实现两种类型的输入源，您可以按原样使用它们。两个来源之间的唯一区别是它们如何发出信号。基于端口的源由内核自动发出信号，并且必须从另一个线程手动发信号通知自定义源。</p>
<p>创建输入源时，将其分配给运行循环的一个或多个模式。模式会影响在任何给定时刻监视哪些输入源。大多数情况下，您在默认模式下运行运行循环，但您也可以指定自定义模式。如果输入源未处于当前监视模式，则会保留它生成的任何事件，直到运行循环以正确模式运行。</p>
<p>以下部分描述了一些输入源。</p>
<h4><a id="%E5%9F%BA%E4%BA%8E%E7%AB%AF%E5%8F%A3%E7%9A%84%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>基于端口的源</h4>
<p>Cocoa 和 Core Foundation 提供内置支持，使用与端口相关的对象和函数创建基于端口的输入源。例如，在 Cocoa中，您根本不必直接创建输入源。您只需创建一个端口对象，并使用 <code>NSPort</code> 的方法将该端口添加到运行循环中。<code>port</code> 对象为您处理所需输入源的创建和配置。</p>
<p>在 Core Foundation 中，您必须手动创建端口及其运行循环源。在这两种情况下，都使用与端口 opaque 类型（<code>CFMachPortRef</code>，<code>CFMessagePortRef</code> 或 <code>CFSocketRef</code>）关联的函数来创建适当的对象。</p>
<p>有关如何设置和配置基于端口的自定义源的示例，请参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-131281">配置基于端口的输入源</a>。</p>
<h4><a id="%E8%87%AA%E5%AE%9A%E4%B9%89%E8%BE%93%E5%85%A5%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>自定义输入源</h4>
<p>要创建自定义输入源，必须在 Core Foundation 中使用与 <code>CFRunLoopSourceRef</code> opaque 类型关联的函数。您可以使用多个回调函数配置自定义输入源。Core Foundation 在不同的点调用这些函数来配置源，处理任何传入事件，并在从运行循环中删除源时拆除源。</p>
<p>除了在事件到达时定义自定义源的行为外，还必须定义事件传递机制。源的这一部分在一个单独的线程上运行，负责为输入源提供其数据，并在数据准备好进行处理时发出信号。事件传递机制取决于您，但不必过于复杂。</p>
<p>有关如何创建自定义输入源的示例，请参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW3">定义自定义输入源</a>。有关自定义输入源的参考信息，另请参阅 <a href="https://developer.apple.com/documentation/corefoundation/cfrunloopsource-rhr">CFRunLoopSource参考</a>。</p>
<h4><a id="cocoa%E6%89%A7%E8%A1%8C%E9%80%89%E6%8B%A9%E5%99%A8%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Cocoa 执行选择器源</h4>
<p>除了基于端口的源之外，Cocoa 还定义了一个自定义输入源，允许您在任何线程上执行选择器。与基于端口的源类似，执行选择器请求在目标线程上被序列化，从而减轻了在一个线程上运行多个方法时可能发生的许多同步问题。<u>与基于端口的源不同，执行选择器源在执行其选择器后将其自身从运行循环中移除。</u></p>
<blockquote>
<p>注意：在 OS X v10.5 之前，执行选择器源主要用于向主线程发送消息，但在 OS X v10.5 及更高版本和 iOS 中，您可以使用它们将消息发送到任何线程。</p>
</blockquote>
<p><u>在另一个线程上执行选择器时，目标线程必须具有活动的运行循环。</u>对于您创建的线程，这意味着要等到代码显式启动运行循环。但是，因为主线程启动了自己的运行循环，所以只要应用程序调用应用程序委托的 <code>applicationDidFinishLaunching:</code> 方法，就可以开始在该线程上发出调用。运行循环每次通过循环处理所有排队的执行选择器调用，而不是在每次循环迭代期间处理一个。</p>
<p>表 3-2 列出了在 <code>NSObject</code> 上定义的可用于在其他线程上执行选择器的方法。因为这些方法是在 <code>NSObject</code> 上声明的，所以您可以在任何可以访问 objectivec 对象的线程中使用它们，包括 POSIX 线程。这些方法实际上并不创建新线程来执行选择器。
有关每种方法的详细信息，请参阅 <a href="https://developer.apple.com/documentation/objectivec/nsobject">NSObject 类参考</a>。</p>
<p>表 3-2：在其他线程上执行选择器</p>
<table>
<thead>
<tr>
<th>方法</th>
<th style="text-align: center">描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>performSelectorOnMainThread:withObject:waitUntilDone:</code> <code>performSelectorOnMainThread:withObject:waitUntilDone:modes:</code></td>
<td style="text-align: center">在该线程的下一个运行循环周期中，在应用程序的主线程上执行指定的选择器。这些方法为您提供了阻止当前线程直到执行选择器的选项。</td>
</tr>
<tr>
<td><code>performSelector:onThread:withObject:waitUntilDone:</code> <code>performSelector:onThread:withObject:waitUntilDone:modes:</code></td>
<td style="text-align: center">在具有 <code>NSThread</code> 对象的任何线程上执行指定的选择器。这些方法为您提供了阻止当前线程直到执行选择器的选项。</td>
</tr>
<tr>
<td><code>performSelector:withObject:afterDelay:</code> <code>performSelector:withObject:afterDelay:inModes:</code></td>
<td style="text-align: center">在下一个运行循环周期和可选的延迟周期之后，在当前线程上执行指定的选择器。因为它等待直到下一个运行循环周期来执行选择器，所以这些方法提供了来自当前执行代码的自动短暂延迟。多个排队选择器按排队顺序依次执行。</td>
</tr>
<tr>
<td><code>cancelPreviousPerformRequestsWithTarget:</code> <code>cancelPreviousPerformRequestsWithTarget:selector:object:</code></td>
<td style="text-align: center">允许您使用 <code>performSelector:withObject:afterDelay:</code> 或 <code>performSelector:withObject:afterDelay:inModes:</code> 方法。</td>
</tr>
</tbody>
</table>
<h3><a id="%E5%AE%9A%E6%97%B6%E5%99%A8%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>定时器源</h3>
<p>定时器源在将来的预设时间将同步事件传递给您的线程。定时器是线程通知自己做某事的一种方式。例如，一旦在来自用户的连续点击之间经过了一定量的时间，搜索字段就可以使用定时器来启动自动搜索。使用此延迟时间使用户有机会在开始搜索之前输入尽可能多的所需搜索字符串。</p>
<p>虽然它生成基于时间的通知，但定时器不是实时机制。与输入源类似，定时器与运行循环的特定模式相关联。如果定时器未处于运行循环当前正在监视的模式，则在您使用其中一个定时器支持的模式运行运行循环之前，它不会触发。类似地，如果定时器在运行循环处于执行处理程序例程的过程中触发，则定时器将等待直到下一次通过运行循环来调用其处理程序例程。如果运行循环根本没有运行，则定时器永远不会触发。</p>
<p>您可以将定时器配置为仅生成一次或重复生成事件。重复定时器根据计划的开始时间自动重新设置自己，而不是实际开始时间。例如，如果计划在特定时间和之后每5秒计时一次定时器，则计划的开始时间将始终落在原始的5秒时间间隔上，即使实际开始时间延迟。如果开始时间延迟太多以至于错过了一个或多个预定开始时间，则定时器仅在错过的时间段内开始一次。在为错过的时段开始之后，定时器被重新安排用于下一个预定的开始时间。</p>
<p>有关配置定时器源的更多信息，请参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW6">配置定时器源</a>。有关参考信息，请参阅 <a href="https://developer.apple.com/documentation/foundation/timer">NSTimer 类参考</a>或<a href="https://developer.apple.com/documentation/corefoundation/cfrunlooptimer-rhk"> CFRunLoopTimer 参考</a>。</p>
<h3><a id="%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF%E8%A7%82%E5%AF%9F%E5%99%A8" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>运行循环观察器</h3>
<p>与在发生适当的异步或同步事件时触发的源相反，运行循环观察器在执行运行循环期间在特殊位置触发。您可以使用运行循环观察器来准备线程以处理给定事件或在线程进入休眠状态之前准备线程。您可以将运行循环观察器与运行循环中的以下事件相关联：</p>
<ul>
<li>运行循环的入口。</li>
<li>当运行循环即将处理定时器时。</li>
<li>当运行循环即将处理输入源时。</li>
<li>当运行循环即将进入休眠状态时。</li>
<li>当运行循环唤醒时，但在它处理唤醒它的事件之前。</li>
<li>从运行循环退出。</li>
</ul>
<p>您可以使用 Core Foundation 将运行循环观察器添加到应用程序。要创建运行循环观察器，请创建 <code>CFRunLoopObserverRef</code> opaque 类型的新实例。此类型会跟踪您的自定义回调函数及其感兴趣的活动。</p>
<p>与定时器类似，运行循环观察器可以使用一次或重复使用。一次性观察者在发射后将其自身从运行循环中移除，而重复的观察者仍然附着。您可以指定观察者在创建时运行一次还是重复运行。</p>
<p>有关如何创建运行循环观察器的示例，请参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW18">配置运行循环</a>。有关参考信息，请参阅 <a href="https://developer.apple.com/documentation/corefoundation/cfrunloopobserver">CFRunLoopObserver 参考</a>。</p>
<h3><a id="%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF%E4%BA%8B%E4%BB%B6%E5%BA%8F%E5%88%97" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>运行循环事件序列</h3>
<p>每次运行它时，线程的运行循环都会处理挂起的事件，并为任何附加的观察者生成通知。它执行此操作的顺序非常具体，如下所示：</p>
<ol>
<li>通知观察者已经输入了运行循环。</li>
<li>通知观察者任何准备好的定时器即将开始计时。</li>
<li>通知观察者任何非基于端口的输入源即将触发。</li>
<li>触发任何准备触发的基于非端口的输入源。</li>
<li>如果基于端口的输入源准备就绪并等待触发，请立即处理该事件。转到第 9 步。</li>
<li>通知观察者线程即将睡眠。</li>
<li>将线程置于睡眠状态，直到发生以下事件之一：
<ul>
<li>事件到达基于端口的输入源。</li>
<li>定时器运行。</li>
<li>为运行循环设置的超时值到期。</li>
<li>运行循环被明确唤醒。</li>
</ul>
</li>
<li>通知观察者线程刚刚被唤醒。</li>
<li>处理待处理事件。
<ul>
<li>如果触发了用户定义的定时器，则处理定时器事件并重新启动循环。转到第 2 步。</li>
<li>如果输入源被触发，则传递该事件。</li>
<li>如果运行循环被明确唤醒但尚未超时，请重新启动循环。转到第2步。</li>
<li>通知观察者运行循环已退出。</li>
</ul>
</li>
</ol>
<p>由于定时器和输入源的观察者通知是在事件实际发生之前传递的，因此通知时间与实际事件的时间之间可能存在差距。如果这些事件之间的时间关系很重要，您可以使用睡眠和唤醒睡眠通知来帮助您关联实际事件之间的时间。</p>
<p>因为在运行运行循环时会传递定时器和其他周期性事件，所以绕过该循环会中断这些事件的传递。每当您通过输入循环并重复从应用程序请求事件来实现鼠标跟踪例程时，就会出现此行为的典型示例。因为您的代码直接抓取事件，而不是让应用程序正常调度这些事件，所以在鼠标跟踪例程退出并将控制权返回给应用程序之后，活动定时器将无法触发。</p>
<p>可以使用运行循环对象显式唤醒运行循环。其他事件也可能导致运行循环被唤醒。例如，添加另一个非基于端口的输入源会唤醒运行循环，以便可以立即处理输入源，而不是等到其他事件发生。</p>
<h2><a id="%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E4%BD%BF%E7%94%A8%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么时候使用运行循环？</h2>
<p>您需要显式运行运行循环的唯一时间是为应用程序创建辅助线程。应用程序主线程的运行循环是一个至关重要的基础架构。因此，app 框架提供了运行主应用程序循环的代码并自动启动该循环。iOS 中的 <code>UIApplication</code> 的运行方法（或 OS X 中的 <code>NSApplication</code> ）启动应用程序的主循环作为正常启动序列的一部分。如果您使用 Xcode 模板项目来创建应用程序，则永远不必显式调用这些例程。</p>
<p>对于辅助线程，您需要确定是否需要运行循环，如果是，请自行配置并启动它。在所有情况下，您都不需要启动线程的运行循环。例如，如果使用线程执行某些长时间运行且预定义的任务，则可以避免启动运行循环。<u>运行循环适用于您希望与线程进行更多交互的情况。</u>例如，如果您计划执行以下任何操作，则需要启动运行循环：</p>
<ul>
<li>使用端口或自定义输入源与其他线程通信。</li>
<li>在线程上使用定时器。</li>
<li>在 Cocoa 应用程序中使用任何 <code>performSelector...</code> 方法。</li>
<li>保持线程以执行定期任务。</li>
</ul>
<p>如果您确实选择使用运行循环，则配置和设置非常简单。与所有线程编程一样，您应该有一个在适当情况下退出辅助线程的计划。最好通过让它退出而不是强制它终止来干净地结束一个线程。有关如何配置和退出运行循环的信息，请参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW5">使用运行循环对象</a>。</p>
<h2><a id="%E4%BD%BF%E7%94%A8%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF%E5%AF%B9%E8%B1%A1" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>使用运行循环对象</h2>
<p>运行循环对象提供了主要接口，用于将输入源，定时器和运行循环观察器添加到运行循环，然后运行它。每个线程都有一个与之关联的运行循环对象。在 Cocoa 中，此对象是 <code>NSRunLoop</code> 类的实例。在低级应用程序中，它是指向 <code>CFRunLoopRef</code> opaque 类型的指针。</p>
<h3><a id="%E8%8E%B7%E5%8F%96%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF%E5%AF%B9%E8%B1%A1" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>获取运行循环对象</h3>
<p>要获取当前线程的运行循环，请使用以下方法之一：</p>
<ul>
<li>在 Cocoa 应用程序中，使用 <code>NSRunLoop</code> 的 <code>currentRunLoop</code> 类方法来检索 <code>NSRunLoop</code> 对象。</li>
<li>使用 <code>CFRunLoopGetCurrent</code> 函数。</li>
</ul>
<h3><a id="%E9%85%8D%E7%BD%AE%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>配置运行循环</h3>
<p>在辅助线程上运行运行循环之前，必须至少添加一个输入源或定时器。如果运行循环没有要监视的任何源，则在您尝试运行它时会立即退出。有关如何将源添加到运行循环的示例，请参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW7">配置运行循环源</a>。</p>
<p>除了安装源之外，您还可以安装运行循环观察器并使用它们来检测运行循环的不同执行阶段。要安装运行循环观察器，可以创建 <code>CFRunLoopObserverRef</code> opaque 类型，并使用 <code>CFRunLoopAddObserver</code> 函数将其添加到运行循环中。必须使用 Core Foundation 创建运行循环观察器，即使对于 Cocoa 应用程序也是如此。</p>
<p>表 3-1 显示了一个将运行循环观察器附加到其运行循环的线程的主例程。该示例的目的是向您展示如何创建一个运行循环观察器，因此代码只是设置一个运行循环观察器来监视所有运行循环活动。基本处理程序例程（未显示）只是在处理定时器请求时记录运行循环活动。</p>
<p>表 3-1：创建一个运行循环观察器</p>
<pre><code class="language-objectivec">- (void)threadMain
{
    // The application uses garbage collection, so no autorelease pool is needed.
    NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
    // Create a run loop observer and attach it to the run loop.
    CFRunLoopObserverContext  context = {0, self, NULL, NULL, NULL};
    CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
            kCFRunLoopAllActivities, YES, 0, &amp;myRunLoopObserver, &amp;context);
 
    if (observer)
    {
        CFRunLoopRef    cfLoop = [myRunLoop getCFRunLoop];
        CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
    }
 
    // Create and schedule the timer.
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
                selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
 
    NSInteger    loopCount = 10;
    do
    {
        // Run the run loop 10 times to let the timer fire.
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
        loopCount--;
    }
    while (loopCount);
}
</code></pre>
<p>为长期存在的线程配置运行循环时，最好添加至少一个输入源来接收消息。虽然您只能连接一个定时器进入运行循环，但一旦定时器触发，它通常会失效，这会导致运行循环退出。附加重复定时器可以使运行循环运行更长的时间，但是会涉及定期触发定时器以唤醒您的线程，这实际上是另一种形式的轮询。相比之下，输入源会等待事件发生，让线程保持睡眠状态。</p>
<h3><a id="%E5%90%AF%E5%8A%A8%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>启动运行循环</h3>
<p>只有应用程序中的辅助线程才需要启动运行循环。运行循环必须至少有一个输入源或定时器才能进行监视。如果没有连接，则运行循环立即退出。</p>
<p>有几种方法可以启动运行循环，包括：</p>
<ul>
<li>无条件</li>
<li>设定时限</li>
<li>在特定模式下</li>
</ul>
<p>无条件地输入您的运行循环是最简单的选择，但它也是最不可取的选择。无条件地运行您的运行循环会将线程置于永久循环中，这使您几乎无法控制运行循环本身。您可以添加和删除输入源和定时器，但停止运行循环的唯一方法是终止它。也无法在自定义模式下运行运行循环。</p>
<p>比起无条件地运行运行循环，最好使用超时值运行运行循环。使用超时值时，运行循环将一直运行，直到事件到达或分配的时间到期。如果事件到达，则将该事件分派给处理程序进行处理，然后退出运行循环。然后，您的代码可以重新启动运行循环以处理下一个事件。如果指定的时间到期，您只需重新启动运行循环或使用时间进行任何所需的内务处理。</p>
<p>除了超时值，您还可以使用特定模式运行运行循环。模式和超时值不是互斥的，可以在启动运行循环时使用。模式限制将事件传递到运行循环的源类型，并在<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW12">运行循环模式</a>中进行了更详细的描述。</p>
<p>表 3-2 显示了线程主入口例程的框架版本。此示例的关键部分显示了运行循环的基本结构。实质上，您将输入源和定时器添加到运行循环中，然后重复调用其中一个例程以启动运行循环。每次运行循环例程返回时，您都会检查是否出现了可能需要退出该线程的任何条件。该示例使用 Core Foundation 运行循环例程，以便它可以检查返回结果并确定运行循环退出的原因。如果使用 Cocoa 并且不需要检查返回值，也可以使用 <code>NSRunLoop</code> 类的方法以类似的方式运行运行循环。（有关调用 <code>NSRunLoop</code> 类的方法的运行循环的示例，请参阅表 3-14。）</p>
<p>表 3-2：运行运行循环</p>
<pre><code class="language-objectivec">- (void)skeletonThreadMain
{
    // Set up an autorelease pool here if not using garbage collection.
    BOOL done = NO;
 
    // Add your sources or timers to the run loop and do any other setup.
 
    do
    {
        // Start the run loop but return after each source is handled.
        SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
 
        // If a source explicitly stopped the run loop, or if there are no
        // sources or timers, go ahead and exit.
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
            done = YES;
 
        // Check for any other exit conditions here and set the
        // done variable as needed.
    }
    while (!done);
 
    // Clean up code here. Be sure to release any allocated autorelease pools.
}
</code></pre>
<p>可以递归地运行运行循环。换句话说，您可以调用 <code>CFRunLoopRun</code>，<code>CFRunLoopRunInMode</code> 或任何 <code>NSRunLoop</code> 方法，以便从输入源或定时器的处理程序例程中启动运行循环。执行此操作时，您可以使用任何要运行嵌套运行循环的模式，包括外部运行循环使用的模式。</p>
<h3><a id="%E9%80%80%E5%87%BA%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>退出运行循环</h3>
<p>在处理事件之前，有两种方法可以使运行循环退出：</p>
<ul>
<li>配置运行循环以使用超时值运行。</li>
<li>告诉运行循环停止。</li>
</ul>
<p>如果您可以管理它，那么使用超时值肯定是首选。指定超时值可让运行循环完成所有正常处理，包括在退出之前向运行循环观察器发送通知。</p>
<p>使用 <code>CFRunLoopStop</code> 函数显式停止运行循环会产生类似于超时的结果。运行循环发出任何剩余的运行循环通知，然后退出。不同之处在于，您可以在无条件启动的运行循环中使用此技术。</p>
<p>虽然删除运行循环的输入源和定时器也可能导致运行循环退出，但这不是停止运行循环的可靠方法。一些系统例程将输入源添加到运行循环以处理所需的事件。因为您的代码可能不知道这些输入源，所以它将无法删除它们，这将阻止运行循环退出。</p>
<h3><a id="%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E5%92%8C%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF%E5%AF%B9%E8%B1%A1" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>线程安全和运行循环对象</h3>
<p>线程安全性取决于您用来操作运行循环的 API。Core Foundation 中的函数通常是线程安全的，可以从任何线程调用。但是，如果您正在执行更改运行循环配置的操作，那么尽可能从拥有运行循环的线程执行此操作仍然是一种好习惯。</p>
<p>Cocoa <code>NSRunLoop</code> 类不像 Core Foundation 对应的那样具有固有的线程安全性。如果您使用 <code>NSRunLoop</code> 类来修改运行循环，则应仅从拥有该运行循环的同一线程执行此操作。将输入源或定时器添加到属于不同线程的运行循环可能会导致代码崩溃或以意外方式运行。</p>
<h2><a id="%E9%85%8D%E7%BD%AE%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>配置运行循环源</h2>
<p>以下部分显示了如何在 Cocoa 和 Core Foundation 中设置不同类型的输入源的示例。</p>
<h3><a id="%E5%AE%9A%E4%B9%89%E4%B8%80%E4%B8%AA%E8%87%AA%E5%AE%9A%E4%B9%89%E8%BE%93%E5%85%A5%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>定义一个自定义输入源</h3>
<p>创建自定义输入源涉及定义以下内容：</p>
<ul>
<li>您希望输入源处理的信息。</li>
<li>一个调度程序例程，让感兴趣的客户知道如何联系您的输入源。</li>
<li>执行任何客户端发送的请求的处理程序例程。</li>
<li>取消例程以使输入源无效。</li>
</ul>
<p>由于您创建了一个自定义输入源来处理自定义信息，因此实际配置的设计非常灵活。调度程序，处理程序和取消例程是您自定义输入源几乎总是需要的关键例程。但是，大多数输入源行为的其余部分都发生在那些处理程序例程之外。例如，您可以定义将数据传递到输入源以及将输入源的数据传递给其他线程的机制。</p>
<p>图 3-2 显示了自定义输入源的示例配置。在此示例中，应用程序的主线程维护对输入源的引用，该输入源的自定义命令缓冲区以及安装输入源的运行循环。当主线程有一个任务，它想要传递给工作线程时，它会向命令缓冲区发布一个命令以及工作线程启动任务所需的任何信息。（因为工作线程的主线程和输入源都可以访问命令缓冲区，所以必须同步该访问。）一旦命令发布，主线程就会发出信号输入源并唤醒工作线程的运行循环。收到唤醒命令后，运行循环调用输入源的处理程序，该处理程序处理命令缓冲区中的命令。</p>
<p>图 3-2 操作一个自定义输入源：</p>
<p><img src="https://ushio.oss-cn-shanghai.aliyuncs.com/MWeb/Threading_Programming_Guide/custominputsource.jpg" alt="custominputsource" /></p>
<p>以下部分介绍了上图中自定义输入源的实现，并显示了您需要实现的关键代码。</p>
<h4><a id="%E5%AE%9A%E4%B9%89%E8%BE%93%E5%85%A5%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>定义输入源</h4>
<p>定义自定义输入源需要使用 Core Foundation 例程来配置运行循环源并将其附加到运行循环。虽然基本处理程序是基于 C 的函数，但这并不妨碍您为这些函数编写包装器并使用 objectivec 或 C++ 来实现代码体。</p>
<p>图 3-2 中引入的输入源使用 objectivec 对象来管理命令缓冲区并与运行循环协调。表 3-3 显示了该对象的定义。<code>RunLoopSource</code> 对象管理命令缓冲区并使用该缓冲区从其他线程接收消息。此清单还显示了 <code>RunLoopContext</code> 对象的定义，该对象实际上只是一个容器对象，用于传递 <code>RunLoopSource</code> 对象和对应用程序主线程的运行循环引用。</p>
<p>表 3-3：定义自定义输入源对象</p>
<pre><code class="language-objectivec">@interface RunLoopSource : NSObject
{
    CFRunLoopSourceRef runLoopSource;
    NSMutableArray* commands;
}
 
- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;
 
// Handler method
- (void)sourceFired;
 
// Client interface for registering commands to process
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
 
@end
 
// These are the CFRunLoopSourceRef callback functions.
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
 
// RunLoopContext is a container object used during registration of the input source.
@interface RunLoopContext : NSObject
{
    CFRunLoopRef        runLoop;
    RunLoopSource*        source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;
 
- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end
</code></pre>
<p>尽管 objectivec 代码管理输入源的自定义数据，但将输入源附加到运行循环需要基于 C 的回调函数。当您将运行循环源实际附加到运行循环时，将调用这些函数中的第一个，如表 3-4 所示。因为此输入源只有一个客户端（主线程），所以它使用调度程序函数发送消息以使用该线程上的应用程序委托注册自身。当委托想要与输入源通信时，它使用 <code>RunLoopContext</code> 对象中的信息来执行此操作。</p>
<p>表 3-4：调度运行循环源</p>
<pre><code class="language-objectivec">void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
    RunLoopSource* obj = (RunLoopSource*)info;
    AppDelegate*   del = [AppDelegate sharedAppDelegate];
    RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
 
    [del performSelectorOnMainThread:@selector(registerSource:)
                                withObject:theContext waitUntilDone:NO];
}
</code></pre>
<p>最重要的回调例程之一是用于在输入源发出信号时处理自定义数据的例程。表 3-5 显示了与 <code>RunLoopSource</code> 对象关联的执行回调例程。此函数只是将执行请求的请求转发给 <code>sourceFired</code> 方法，然后该方法处理命令缓冲区中存在的任何命令。</p>
<p>表 3-5：在输入源中执行工作</p>
<pre><code class="language-objectivec">void RunLoopSourcePerformRoutine (void *info)
{
    RunLoopSource*  obj = (RunLoopSource*)info;
    [obj sourceFired];
}
</code></pre>
<p>如果使用 <code>CFRunLoopSourceInvalidate</code> 函数从运行循环中删除输入源，系统将调用输入源的取消例程。您可以使用此例程通知客户端您的输入源不再有效，并且应该删除对它的任何引用。表 3-6 显示了向 <code>RunLoopSource</code> 对象注册的取消回调例程。此函数将另一个 <code>RunLoopContext</code> 对象发送到应用程序委托，但这次要求委托删除对运行循环源的引用。</p>
<p>表 3-6：使输入源无效</p>
<pre><code class="language-objectivec">void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
    RunLoopSource* obj = (RunLoopSource*)info;
    AppDelegate* del = [AppDelegate sharedAppDelegate];
    RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
 
    [del performSelectorOnMainThread:@selector(removeSource:)
                                withObject:theContext waitUntilDone:YES];
}
</code></pre>
<blockquote>
<p>注意：应用程序委托的 <code>registerSource:</code> 和 <code>removeSource:</code> 方法的代码显示在<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW37">与输入源的客户端协调</a> 中。</p>
</blockquote>
<h4><a id="%E5%9C%A8%E8%BF%90%E8%A1%8C%E5%BE%AA%E7%8E%AF%E4%B8%8A%E5%AE%89%E8%A3%85%E8%BE%93%E5%85%A5%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>在运行循环上安装输入源</h4>
<p>表 3-7 显示了 <code>RunLoopSource</code> 类的 <code>init</code> 和 <code>addToCurrentRunLoop</code> 方法。<code>init</code> 方法创建 <code>CFRunLoopSourceRef</code> opaque 类型，该类型必须实际附加到运行循环。它将 <code>RunLoopSource</code> 对象本身作为上下文信息传递，以便回调例程具有指向该对象的指针。在工作线程调用 <code>addToCurrentRunLoop</code> 方法之前不会安装输入源，此时将调用 <code>RunLoopSourceScheduleRoutine</code> 回调函数。一旦输入源被添加到运行循环中，线程就可以运行其运行循环来等待它。</p>
<p>表 3-7：安装运行循环源</p>
<pre><code class="language-objectivec">- (id)init
{
    CFRunLoopSourceContext    context = {0, self, NULL, NULL, NULL, NULL, NULL,
                                        &amp;RunLoopSourceScheduleRoutine,
                                        RunLoopSourceCancelRoutine,
                                        RunLoopSourcePerformRoutine};
 
    runLoopSource = CFRunLoopSourceCreate(NULL, 0, &amp;context);
    commands = [[NSMutableArray alloc] init];
 
    return self;
}
 
- (void)addToCurrentRunLoop
{
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}
</code></pre>
<h4><a id="%E4%B8%8E%E8%BE%93%E5%85%A5%E6%BA%90%E7%9A%84%E5%AE%A2%E6%88%B7%E5%8D%8F%E8%B0%83" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>与输入源的客户协调</h4>
<p>要使输入源有用，您需要对其进行操作并从另一个线程发出信号。输入源的重点是将其关联的线程置于休眠状态，直到有事情要做。这个事实需要让应用程序中的其他线程知道输入源并有办法与之通信。</p>
<p>通知客户端输入源的一种方法是在输入源首次安装到其运行循环时发出注册请求。您可以使用任意数量的客户端注册输入源，或者只需将其注册到某个中央代理商，然后将您的输入源发送给感兴趣的客户。表 3-8 显示了应用程序委托定义的注册方法，并在调用 <code>RunLoopSource</code> 对象的调度程序函数时调用。此方法接收 <code>RunLoopSource</code> 对象提供的 <code>RunLoopContext</code> 对象，并将其添加到其源列表中。此列表还显示了从运行循环中删除输入源时用于取消注册的例程。</p>
<p>表 3-8：使用应用程序委托注册和删除输入源</p>
<pre><code class="language-objectivec">- (void)registerSource:(RunLoopContext*)sourceInfo;
{
    [sourcesToPing addObject:sourceInfo];
}
 
- (void)removeSource:(RunLoopContext*)sourceInfo
{
    id    objToRemove = nil;
 
    for (RunLoopContext* context in sourcesToPing)
    {
        if ([context isEqual:sourceInfo])
        {
            objToRemove = context;
            break;
        }
    }
 
    if (objToRemove)
        [sourcesToPing removeObject:objToRemove];
}
</code></pre>
<blockquote>
<p>注意：调用前面列表中的方法的回调函数如表 3-4 和表 3-6 所示。</p>
</blockquote>
<h4><a id="%E5%8F%91%E4%BF%A1%E5%8F%B7%E8%BE%93%E5%85%A5%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>发信号输入源</h4>
<p>在将其数据移交给输入源之后，客户端必须向源发信号并唤醒其运行循环。信号源使运行循环知道源已准备好进行处理。并且因为线程可能在信号发生时处于睡眠状态，所以应该总是明确地唤醒运行循环。如果不这样做可能会导致处理输入源的延迟。</p>
<p>表 3-9 显示了 <code>RunLoopSource</code> 对象的 <code>fireCommandsOnRunLoop</code> 方法。当客户端准备好处理他们添加到缓冲区的命令时，客户端会调用此方法。</p>
<p>表 3-9：唤醒运行循环</p>
<pre><code class="language-objectivec">- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
    CFRunLoopSourceSignal(runLoopSource);
    CFRunLoopWakeUp(runloop);
}
</code></pre>
<blockquote>
<p>注意：您不应该通过发送自定义输入源来尝试处理 SIGHUP 或其他类型的进程级信号。用于唤醒运行循环的 Core Foundation 函数不是信号安全的，不应在应用程序的信号处理程序例程中使用。有关信号处理程序例程的更多信息，请参见 <a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/sigaction.2.html#//apple_ref/doc/man/2/sigaction">sigaction</a> 手册页。</p>
</blockquote>
<h3><a id="%E9%85%8D%E7%BD%AE%E5%AE%9A%E6%97%B6%E5%99%A8%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>配置定时器源</h3>
<p>要创建定时器源，您所要做的就是创建一个定时器对象并在运行循环上安排它。在 Cocoa 中，使用 <code>NSTimer</code> 类创建新的定时器对象，在 Core Foundation 中使用 <code>CFRunLoopTimerRef</code> opaque类型。在内部，<code>NSTimer</code> 类只是 Core Foundation 的扩展，它提供了一些便利功能，例如使用相同方法创建和计划定时器的功能。</p>
<p>在 Cocoa 中，您可以使用以下任一类方法一次创建和计划定时器：</p>
<ul>
<li><code>scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:</code></li>
<li><code>scheduledTimerWithTimeInterval:invocation:repeats:</code></li>
</ul>
<p>这些方法创建定时器并将其以默认模式（NSDefaultRunLoopMode）添加到当前线程的运行循环中。如果需要，还可以手动调度定时器，方法是创建 <code>NSTimer</code> 对象，然后使用 <code>NSRunLoop</code> 的 <code>addTimer:forMode:</code> 方法将其添加到运行循环中。这两种技术基本上都是一样的，但是可以对定时器的配置进行不同程度的控制。例如，如果您创建定时器并手动将其添加到运行循环，则可以使用默认模式以外的模式执行此操作。表 3-10 显示了如何使用这两种技术创建定时器。第一个定时器的初始延迟为 1 秒，但之后每 0.1 秒定时触发一次。第二个定时器在最初的 0.2 秒延迟后开始计时，然后每 0.2 秒触发一次。</p>
<p>表 3-10：使用 NSTimer 创建和调度定时器</p>
<pre><code class="language-objectivec">NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
// Create and schedule the first timer.
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
                        interval:0.1
                        target:self
                        selector:@selector(myDoFireTimer1:)
                        userInfo:nil
                        repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
 
// Create and schedule the second timer.
[NSTimer scheduledTimerWithTimeInterval:0.2
                        target:self
                        selector:@selector(myDoFireTimer2:)
                        userInfo:nil
                        repeats:YES];
</code></pre>
<p>表 3-11 显示了使用 Core Foundation 函数配置定时器所需的代码。虽然此示例未在上下文结构中传递任何用户定义的信息，但您可以使用此结构传递定时器所需的任何自定义数据。有关此结构内容的更多信息，请参阅 <a href="https://developer.apple.com/documentation/corefoundation/cfrunlooptimer-rhk">CFRunLoopTimer Reference</a> 中的说明。</p>
<p>表 3-11：使用 Core Foundation 创建和调度定时器</p>
<pre><code class="language-objectivec">CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
                                        &amp;myCFTimerCallback, &amp;context);
 
CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
</code></pre>
<h3><a id="%E9%85%8D%E7%BD%AE%E5%9F%BA%E4%BA%8E%E7%AB%AF%E5%8F%A3%E7%9A%84%E8%BE%93%E5%85%A5%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>配置基于端口的输入源</h3>
<p>Cocoa 和 Core Foundation 都提供了基于端口的对象，用于线程之间或进程之间的通信。以下部分介绍如何使用多种不同类型的端口设置端口通信。</p>
<h4><a id="%E9%85%8D%E7%BD%AEnsmachport%E5%AF%B9%E8%B1%A1" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>配置 NSMachPort 对象</h4>
<p>要与 <code>NSMachPort</code> 对象建立本地连接，请创建端口对象并将其添加到主线程的运行循环中。启动辅助线程时，将同一对象传递给线程的入口点函数。辅助线程可以使用相同的对象将消息发送回主线程。</p>
<h5><a id="%E5%AE%9E%E7%8E%B0%E4%B8%BB%E7%BA%BF%E7%A8%8B%E4%BB%A3%E7%A0%81" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>实现主线程代码</h5>
<p>表 3-12 显示了启动辅助工作线程的主要线程代码。因为 Cocoa 框架执行许多配置端口和运行循环的干预步骤，所以 <code>launchThread</code> 方法明显比其 Core Foundation 等效方法短（表 3-17 ）；然而，两者的行为几乎完全相同。一个区别是，该方法不是直接向工作线程发送本地端口的名称，而是直接发送 <code>NSPort</code> 对象。</p>
<p>表 3-12：主线程运行方法</p>
<pre><code class="language-objectivec">- (void)launchThread
{
    NSPort* myPort = [NSMachPort port];
    if (myPort)
    {
        // This class handles incoming port messages.
        [myPort setDelegate:self];
        // Install the port as an input source on the current run loop.
        [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
        // Detach the thread. Let the worker release the port.
        [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
               toTarget:[MyWorkerClass class] withObject:myPort];
    }
}
</code></pre>
<p>为了在线程之间建立双向通信通道，您可能希望让工作线程在签入消息中将其自己的本地端口发送到主线程。通过接收签入消息，您的主线程可以知道在启动第二个线程时一切顺利，并且还为您提供了向该线程发送更多消息的方法。</p>
<p>表 3-13 显示了主线程的 <code>handlePortMessage:</code> 方法。当数据到达线程自己的本地端口时，将调用此方法。当签入消息到达时，该方法直接从端口消息中检索辅助线程的端口并保存以供以后使用。</p>
<p>表 3-13：处理 Mach 端口消息</p>
<pre><code class="language-objectivec">#define kCheckinMessage 100
 
// Handle responses from the worker thread.
- (void)handlePortMessage:(NSPortMessage *)portMessage
{
    unsigned int message = [portMessage msgid];
    NSPort* distantPort = nil;
 
    if (message == kCheckinMessage)
    {
        // Get the worker thread’s communications port.
        distantPort = [portMessage sendPort];
 
        // Retain and save the worker port for later use.
        [self storeDistantPort:distantPort];
    }
    else
    {
        // Handle other messages.
    }
}
</code></pre>
<h5><a id="%E5%AE%9E%E7%8E%B0%E8%BE%85%E5%8A%A9%E7%BA%BF%E7%A8%8B%E4%BB%A3%E7%A0%81" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>实现辅助线程代码</h5>
<p>对于辅助工作线程，您必须配置线程并使用指定的端口将信息传递回主线程。</p>
<p>表 3-14 显示了设置工作线程的代码。在为线程创建自动释放池之后，该方法创建一个工作对象来驱动线程执行。<code>worker</code> 对象的 <code>sendCheckinMessage:</code> 方法（如<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW10">表 3-15</a> 所示）为工作线程创建一个本地端口，并将一个签入消息发送回主线程。</p>
<p>表 3-14：使用 Mach 端口启动工作线程</p>
<pre><code class="language-objectivec">+(void)LaunchThreadWithPort:(id)inData
{
    NSAutoreleasePool*  pool = [[NSAutoreleasePool alloc] init];
 
    // Set up the connection between this thread and the main thread.
    NSPort* distantPort = (NSPort*)inData;
 
    MyWorkerClass*  workerObj = [[self alloc] init];
    [workerObj sendCheckinMessage:distantPort];
    [distantPort release];
 
    // Let the run loop process things.
    do
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                            beforeDate:[NSDate distantFuture]];
    }
    while (![workerObj shouldExit]);
 
    [workerObj release];
    [pool release];
}
</code></pre>
<p>使用 <code>NSMachPort</code> 时，本地和远程线程可以使用相同的端口对象进行线程之间的单向通信。换句话说，由一个线程创建的本地端口对象成为另一个线程的远程端口对象。</p>
<p>表 3-15 显示了辅助线程的签入例程。此方法为将来的通信设置自己的本地端口，然后将签入消息发送回主线程。该方法使用 <code>LaunchThreadWithPort:</code> 方法中接收的端口对象作为消息的目标。</p>
<p>表 3-15：使用 Mach 端口发送签到消息</p>
<pre><code class="language-objectivec">// Worker thread check-in method
- (void)sendCheckinMessage:(NSPort*)outPort
{
    // Retain and save the remote port for future use.
    [self setRemotePort:outPort];
 
    // Create and configure the worker thread port.
    NSPort* myPort = [NSMachPort port];
    [myPort setDelegate:self];
    [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
 
    // Create the check-in message.
    NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
                                         receivePort:myPort components:nil];
 
    if (messageObj)
    {
        // Finish configuring the message and send it immediately.
        [messageObj setMsgId:setMsgid:kCheckinMessage];
        [messageObj sendBeforeDate:[NSDate date]];
    }
}
</code></pre>
<p>使用 <code>NSMachPort</code> 时，本地和远程线程可以使用相同的端口对象进行线程之间的单向通信。换句话说，由一个线程创建的本地端口对象成为另一个线程的远程端口对象。</p>
<h4><a id="%E9%85%8D%E7%BD%AEnsmessageport%E5%AF%B9%E8%B1%A1" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>配置 NSMessagePort 对象</h4>
<p>要与 <code>NSMessagePort</code> 对象建立本地连接，您不能简单地在线程之间传递端口对象。必须按名称获取远程消息端口。在 Cocoa 中实现这一点需要使用特定名称注册本地端口，然后将该名称传递给远程线程，以便它可以获取适当的端口对象进行通信。表 3-16 显示了在您要使用消息端口的情况下的端口创建和注册过程。</p>
<p>表 3-16：注册消息端口</p>
<pre><code class="language-objectivec">NSPort* localPort = [[NSMessagePort alloc] init];
 
// Configure the object and add it to the current run loop.
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
 
// Register the port using a specific name. The name must be unique.
NSString* localPortName = [NSString stringWithFormat:@&quot;MyPortName&quot;];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
                     name:localPortName];
</code></pre>
<h4><a id="%E5%9C%A8core-foundation%E4%B8%AD%E9%85%8D%E7%BD%AE%E5%9F%BA%E4%BA%8E%E7%AB%AF%E5%8F%A3%E7%9A%84%E8%BE%93%E5%85%A5%E6%BA%90" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>在 Core Foundation 中配置基于端口的输入源</h4>
<p>本节介绍如何使用 Core Foundation 在应用程序的主线程和工作线程之间建立双向通信通道。</p>
<p>表 3-17 显示了应用程序主线程调用以启动工作线程的代码。代码所做的第一件事就是设置一个 <code>CFMessagePortRef</code> opaque 类型来监听来自工作线程的消息。工作线程需要端口的名称来建立连接，以便将字符串值传递给工作。</p>
<p>表  3-17：将 Core Foundation 消息端口附加到新线程</p>
<pre><code class="language-objectivec">#define kThreadStackSize        (8 *4096)
OSStatus MySpawnThread()
{
    // Create a local port for receiving responses.
    CFStringRef myPortName;
    CFMessagePortRef myPort;
    CFRunLoopSourceRef rlSource;
    CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
    Boolean shouldFreeInfo;
    // Create a string with the port name.
    myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR(&quot;com.myapp.MainThread&quot;));
    // Create the port.
    myPort = CFMessagePortCreateLocal(NULL, myPortName,
                &amp;MainThreadResponseHandler, &amp;context, &amp;shouldFreeInfo);
    if (myPort != NULL)
    {
        // The port was successfully created.
        // Now create a run loop source for it.
        rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
        if (rlSource)
        {
            // Add the source to the current run loop.
            CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
            // Once installed, these can be freed.
            CFRelease(myPort);
            CFRelease(rlSource);
        }
    }
    // Create the thread and continue processing.
    MPTaskID        taskID;
    return(MPCreateTask(&amp;ServerThreadEntryPoint,
                    (void*)myPortName, kThreadStackSize, NULL, NULL, NULL, 0,
                    &amp;taskID));
}
</code></pre>
<p>线程的入口点函数。端口名称在当前用户上下文中通常应该是唯一的；否则，你可能会遇到冲突。</p>
<p>安装端口并启动线程后，主线程可以在等待线程签入时继续其常规执行。当签入消息到达时，它将被分派到主线程的 <code>MainThreadResponseHandler</code> 函数，如表 3-18 所示。此函数提取工作线程的端口名称，并为将来的通信创建管道。</p>
<p>表 3-18：收到签到消息</p>
<pre><code class="language-objectivec">#define kCheckinMessage 100
// Main thread port message handler
CFDataRef MainThreadResponseHandler(CFMessagePortRef local, SInt32 msgid,  
							   CFDataRef data,
                                    void* info)
{
    if (msgid == kCheckinMessage)
    {
        CFMessagePortRef messagePort; CFStringRef threadPortName;
        CFIndex bufferLength = CFDataGetLength(data);
        UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0);
        CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
        threadPortName = CFStringCreateWithBytes (NULL, buffer, bufferLength, kCFStringEncodingASCII, FALSE);
        // You must obtain a remote message port by name.
        messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName);
        if (messagePort)
        {
            // Retain and save the thread’s comm port for future reference.
            AddPortToListOfActiveThreads(messagePort);
            // Since the port is retained by the previous function, release
            // it here.
            CFRelease(messagePort);
        }
        // Clean up.
        CFRelease(threadPortName); CFAllocatorDeallocate(NULL, buffer);
    }
    else {  /* Process other messages. */ }
    return NULL;
}
</code></pre>
<p>配置主线程后，剩下的唯一事情就是新创建的工作线程创建自己的端口并签入。表 3-19 显示了工作线程的入口点函数。该函数提取主线程的端口名称，并使用它创建一个返回主线程的远程连接。然后，该函数为自己创建一个本地端口，在该线程的运行循环上安装该端口，并向包含本地端口名称的主线程发送一个签入消息。</p>
<p>表 3-19：设置线程结构</p>
<pre><code class="language-objectivec">OSStatus ServerThreadEntryPoint(void* param)
{
    // Create the remote port to the main thread.
    CFMessagePortRef mainThreadPort;
    CFStringRef portName = (CFStringRef)param;
 
    mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
 
    // Free the string that was passed in param.
    CFRelease(portName);
 
    // Create a port for the worker thread.
    CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR(&quot;com.MyApp.Thread-%d&quot;), MPCurrentTaskID());
 
    // Store the port in this thread’s context info for later reference.
    CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
    Boolean shouldFreeInfo;
    Boolean shouldAbort = TRUE;
 
    CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL,
                myPortName,
                &amp;ProcessClientRequest,
                &amp;context,
                &amp;shouldFreeInfo);
 
    if (shouldFreeInfo)
    {
        // Couldn't create a local port, so kill the thread.
        MPExit(0);
    }
 
    CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
    if (!rlSource)
    {
        // Couldn't create a local port, so kill the thread.
        MPExit(0);
    }
 
    // Add the source to the current run loop.
    CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
 
    // Once installed, these can be freed.
    CFRelease(myPort);
    CFRelease(rlSource);
 
    // Package up the port name and send the check-in message.
    CFDataRef returnData = nil;
    CFDataRef outData;
    CFIndex stringLength = CFStringGetLength(myPortName);
    UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);
 
    CFStringGetBytes(myPortName,
                CFRangeMake(0,stringLength),
                kCFStringEncodingASCII,
                0,
                FALSE,
                buffer,
                stringLength,
                NULL);
 
    outData = CFDataCreate(NULL, buffer, stringLength);
 
    CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL, NULL);
 
    // Clean up thread data structures.
    CFRelease(outData);
    CFAllocatorDeallocate(NULL, buffer);
 
    // Enter the run loop.
    CFRunLoopRun();
}
</code></pre>
<p>一旦进入其运行循环，发送到线程端口的所有未来事件都由 <code>ProcessClientRequest</code> 函数处理。该函数的实现取决于线程的工作类型，这里没有显示。</p>
<div style="padding-top:25px;">
    <div style="float:left;">
        <a style="text-decoration:none" href="%E7%BA%BF%E7%A8%8B%E7%AE%A1%E7%90%86.html">&laquo; 上一章：线程管理</a>
    </div>
    <div style="float:right">
        <a style="text-decoration:none" href="%E5%90%8C%E6%AD%A5.html">下一章：同步 &raquo;</a> 
    </div>
</div>

                  </article>
                  <div class="comments-wrap">
                    <div class="share-comments">
                      

                      

                      
                    </div>
                  </div><!-- end comments wrap -->
              </div>
            </div><!-- end columns -->
      </div><!-- end container -->
    </section>



    <footer class="footer">
        <div class="content has-text-centered">
          <p>
              Copyright &copy; 2019
              Powered by <a target="_blank" href="http://www.mweb.im">MWeb</a>,&nbsp; 
              Theme used <a target="_blank" href="https://bulma.io/">Bulma CSS</a>.
          </p>
        </div>
      </footer>



  













<script src="asset/prism.js"></script>



  
    




  </body>
</html>
